/**
 * Project of Vehicle Routing Problem with Time Windows implements 
 * some of well-known algorithms for solving VRPTW and presents new 
 * one.
 *
 * @author Michal Drobny  
 */

package logic.restriction.timeWindow;

import logic.car.VehicleProvider;
import logic.destination.Destination;
import logic.evaluator.Evaluator;
import logic.evaluator.ZeroEvaluator;
import logic.restriction.enumeration.ChangeCycleVariant;
import logic.restriction.enumeration.IterationType;
import logic.restriction.enumeration.TimeWindowCheckingVariant;
import logic.restriction.interfaces.HasTimeDelay;
import logic.restriction.interfaces.HasTimeWindow;
import logic.route.Cycle;
import logic.route.Route;
import logic.timewindow.TimeWindow;

/**
 * Represents the time window restriction of type Long. This restriction is
 * responsible for storing the actual time at process of building cycle and
 * checks if all time windows of serviced destinations in cycle are passed. All
 * restriction are stored in object RestrictionAdapter.
 * 
 * @author Michal Drobny
 * @date 26.11.2014
 */
public class LongTimeWindowRestriction extends
		TimeWindowRestriction<Long, Double> {

	/**
	 * Constructor.
	 */
	public LongTimeWindowRestriction() {
		this.timeWindow = new TimeWindow<Long>(0l, Long.MAX_VALUE);
		this.currentTime = 0L;
		this.timeEvaluator = new ZeroEvaluator();
		this.checkingVariant = TimeWindowCheckingVariant.STRICTLY;
		this.isResetInNewCycle = false;
	}

	/**
	 * Constructor.
	 * 
	 * @param timeWindow
	 *            The time window
	 * @param currentTime
	 *            The current time
	 * @param timeEvaluator
	 *            The time evaluator
	 */
	public LongTimeWindowRestriction(TimeWindow<Long> timeWindow,
			Long currentTime, Evaluator timeEvaluator, TimeWindowCheckingVariant variant, Boolean isResetInNewCycle) {
		this.timeWindow = timeWindow;
		this.currentTime = currentTime;
		this.timeEvaluator = timeEvaluator;
		this.checkingVariant = variant;
		this.isResetInNewCycle = isResetInNewCycle;
	}

	@Override
	public void iterate(IterationType type, Object... params) {
		switch (type) {
			case DESTINATION_ONLY:
				if (params.length == 2 
					&& checkDestination(params[0]) != null
					&& checkDestination(params[1]) != null
					&& checkLongTimeWindow(params[1]) != null) {
					Destination from = checkDestination(params[0]);
					Destination to = checkDestination(params[1]);
					TimeWindow<Long> timeWindow = checkLongTimeWindow(params[1]);
					Long timeDelay = 0L;
					if (checkLongTimeDelay(params[1]) != null)
						timeDelay = checkLongTimeDelay(params[1]);
					Long newCurrentTime = getCurrentTime() + timeDelay 
							+ Math.round(getTimeEvaluator().evaluate(from, to));
					switch (getCheckingVariant()) {
						case LAZY:
							if (newCurrentTime < timeWindow.getStart())
								this.currentTime = timeWindow.getStart();
							else 
								this.currentTime = newCurrentTime;
							break;
						case STRICTLY:
							this.currentTime = newCurrentTime;
							break;
					}
				}
				break;

			case DESTINATION_TO_START:
				if (params.length == 3 
					&& checkDestination(params[0]) != null
					&& checkDestination(params[1]) != null
					&& checkLongTimeWindow(params[1]) != null) {
					Destination from = checkDestination(params[0]);
					Destination to = checkDestination(params[1]);
					TimeWindow<Long> timeWindow = checkLongTimeWindow(params[1]);
					Long timeDelay = 0L;
					if (checkLongTimeDelay(params[1]) != null)
						timeDelay = checkLongTimeDelay(params[1]);
					Long newCurrentTime = getCurrentTime() + timeDelay 
							+ Math.round(getTimeEvaluator().evaluate(from, to));
					switch (getCheckingVariant()) {
						case LAZY:
							if (newCurrentTime < timeWindow.getStart())
								this.currentTime = timeWindow.getStart();
							else
								this.currentTime = newCurrentTime;
							break;
						case STRICTLY:
							this.currentTime = newCurrentTime;
							break;
					}
				}
				break;

			case ROUTE:
				// Do nothing, should be used canIterate method  
				break;
				
			case CYCLE:
				// Do nothing, should be used canIterate method
				break;
		}
	}

	@Override
	public boolean canIterate(IterationType type, Object... params) {
		switch (type) {
			case DESTINATION_ONLY:
				if (params.length == 2 
					&& checkDestination(params[0]) != null
					&& checkDestination(params[1]) != null
					&& checkLongTimeWindow(params[1]) != null) {
					Destination from = checkDestination(params[0]);
					Destination to = checkDestination(params[1]);
					TimeWindow<Long> timeWindow = checkLongTimeWindow(params[1]);
					Long timeDelay = 0L;
					if (checkLongTimeDelay(params[1]) != null)
						timeDelay = checkLongTimeDelay(params[1]);
					switch (getCheckingVariant()) {
						case LAZY:
							Long newLazyCurrentTimeArrival = timeWindow.getStart();
							Long lazyTimeForMovement = Math.round(getTimeEvaluator().evaluate(from, to));
							if (getCurrentTime() + lazyTimeForMovement >= timeWindow.getStart())
								newLazyCurrentTimeArrival = getCurrentTime() + lazyTimeForMovement;
							Long newLazyCurrentTimeDeparture = newLazyCurrentTimeArrival + timeDelay;
							if (getTimeWindow().include(newLazyCurrentTimeArrival) 
									&& getTimeWindow().include(newLazyCurrentTimeDeparture)
									&& timeWindow.include(newLazyCurrentTimeArrival)
									&& timeWindow.include(newLazyCurrentTimeDeparture))
								return true;
							break;
						case STRICTLY:
							Long newStrictlyCurrentTimeArrival = getCurrentTime() + Math.round(getTimeEvaluator().evaluate(from, to));
							Long newStrictlyCurrentTimeDeparture = newStrictlyCurrentTimeArrival + timeDelay;
							if (getTimeWindow().include(newStrictlyCurrentTimeArrival) 
									&& getTimeWindow().include(newStrictlyCurrentTimeDeparture)
									&& timeWindow.include(newStrictlyCurrentTimeArrival)
									&& timeWindow.include(newStrictlyCurrentTimeDeparture))
								return true;
							break;
					}
				}
				return false;

			case DESTINATION_TO_START:
				if (params.length == 3 
					&& checkDestination(params[0]) != null
					&& checkDestination(params[1]) != null
					&& checkLongTimeWindow(params[1]) != null
					&& checkDestination(params[2]) != null) {
					Destination from = checkDestination(params[0]);
					Destination to = checkDestination(params[1]);
					TimeWindow<Long> timeWindow = checkLongTimeWindow(params[1]);
					Destination start = checkDestination(params[2]);
					Long timeDelay = 0L;
					if (checkLongTimeDelay(params[1]) != null)
						timeDelay = checkLongTimeDelay(params[1]);
					switch (getCheckingVariant()) {
						case LAZY:
							Long newLazyCurrentTimeArrival = timeWindow.getStart();
							Long lazyTimeForMovement = Math.round(getTimeEvaluator().evaluate(from, to));
							if (getCurrentTime() + lazyTimeForMovement >= timeWindow.getStart())
								newLazyCurrentTimeArrival = getCurrentTime() + lazyTimeForMovement;
							Long newLazyCurrentTimeDeparture = newLazyCurrentTimeArrival + timeDelay;
							Long newLazyCurrentTimeToStart = newLazyCurrentTimeDeparture
									+ Math.round(getTimeEvaluator().evaluate(to, start));
							if (getTimeWindow().include(newLazyCurrentTimeArrival) 
								&& getTimeWindow().include(newLazyCurrentTimeToStart)
								&& timeWindow.include(newLazyCurrentTimeArrival)
								&& timeWindow.include(newLazyCurrentTimeDeparture))
								return true;
							break;
						case STRICTLY:
							Long newStrictlyCurrentTimeArrival = getCurrentTime() + Math.round(getTimeEvaluator().evaluate(from, to));
							Long newStrictlyCurrentTimeDeparture = newStrictlyCurrentTimeArrival + timeDelay;
							Long newStrictlyCurrentTimeToStart = newStrictlyCurrentTimeDeparture
									+ Math.round(getTimeEvaluator().evaluate(to, start));
							if (getTimeWindow().include(newStrictlyCurrentTimeArrival) 
								&& getTimeWindow().include(newStrictlyCurrentTimeToStart)
								&& timeWindow.include(newStrictlyCurrentTimeArrival)
								&& timeWindow.include(newStrictlyCurrentTimeDeparture))
								return true;
							break;
					}
				}
				return false;

			case ROUTE:
				if (params.length == 3 && checkRoute(params[0]) != null && checkVehicleProvider(params[1]) != null && checkDestination(params[2]) != null) {
					Route route = checkRoute(params[0]);
					VehicleProvider vehicleProvider = checkVehicleProvider(params[1]);
					Destination start = checkDestination(params[2]);
					if (route.getDestinations() == null || route.getDestinations().isEmpty())
						return true;
					int destinationIndex = 0;
					if (route.getDestinations().size() > 1)
						destinationIndex = 1;
					Destination from = start;
					Long oldDeparture = getCurrentTime();
					Long arrival = getCurrentTime();
					Long departure = getCurrentTime();
					Destination to;
					
					Destination[] orderedDestinations = route.getOrderedDestinations(start);
					if (orderedDestinations == null)
						return false;
					while (destinationIndex < orderedDestinations.length) {
						to = orderedDestinations[destinationIndex];
						Long delay = 0L;
						if (checkLongTimeDelay(to) != null)
							delay = checkLongTimeDelay(to);
						if (checkLongTimeWindow(to) == null)
							return false;
						TimeWindow<Long> timeWindow = checkLongTimeWindow(to);
						arrival = oldDeparture + Math.round(getTimeEvaluator().evaluate(from, to));
						switch (getCheckingVariant()) {
							case LAZY:
								if (arrival < timeWindow.getStart())
									arrival = timeWindow.getStart();
								break;
							default:
						}
						departure = arrival + delay;
						
						if (!getTimeWindow().include(oldDeparture) || !getTimeWindow().include(departure) ||
								!timeWindow.include(arrival) || !timeWindow.include(departure))
							return false;
						
						if (to.equals(start))
							if (canNewCycle(ChangeCycleVariant.VEHICLE_PROVIDER, vehicleProvider)) {
								newCycle(ChangeCycleVariant.VEHICLE_PROVIDER, vehicleProvider);
								oldDeparture = getCurrentTime();
							} else
								return false;
						else
							oldDeparture = departure;
						
						from = to;
						destinationIndex++;
					}
					
					departure = oldDeparture + Math.round(getTimeEvaluator().evaluate(from, start));
					if (!getTimeWindow().include(oldDeparture) || !getTimeWindow().include(departure))
						return false;
					vehicleProvider.reset();
				} else
					return false;
				return true;
				
			case CYCLE:
				if (params.length == 1 && checkCycle(params[0]) != null) {
					Cycle cycle = checkCycle(params[0]);
					if (cycle.getDestinations() == null || cycle.getDestinations().isEmpty())
						return true;
					Destination start = cycle.getDestinations().get(0);
					int destinationIndex = 0;
					if (cycle.getDestinations().size() > 1)
						destinationIndex = 1;
					Destination from = cycle.getDestinations().get(0);
					Long oldDeparture = getCurrentTime();
					Long arrival = getCurrentTime();
					Long departure = getCurrentTime();
					Destination to;
					while (destinationIndex < cycle.getDestinations().size()) {
						to = cycle.getDestinations().get(destinationIndex);
						Long delay = 0L;
						if (checkLongTimeDelay(to) != null)
							delay = checkLongTimeDelay(to);
						if (checkLongTimeWindow(to) == null)
							return false;
						TimeWindow<Long> timeWindow = checkLongTimeWindow(to);
						arrival = oldDeparture + Math.round(getTimeEvaluator().evaluate(from, to));
						switch (getCheckingVariant()) {
							case LAZY:
								if (arrival < timeWindow.getStart())
									arrival = timeWindow.getStart();
								break;
							default:
						}
						departure = arrival + delay;
						
						if (!getTimeWindow().include(oldDeparture) || !getTimeWindow().include(departure) ||
								!timeWindow.include(arrival) || !timeWindow.include(departure))
							return false;
						oldDeparture = departure;
						from = to;
						destinationIndex++;
					}
					
					departure = oldDeparture + Math.round(getTimeEvaluator().evaluate(from, start));
					if (!getTimeWindow().include(oldDeparture) || !getTimeWindow().include(departure))
						return false;
				} else
					return false;
				return true;
			default:
				return false;
		}
	}

	@Override
	public void newCycle(ChangeCycleVariant type, Object... params) {
		switch (type) {
		case VEHICLE_PROVIDER:
			if (params.length == 1 && checkVehicleProvider(params[0]) != null) {
				VehicleProvider vp = (VehicleProvider) params[0];
				if (vp.nextVehicle(true)) {
					if (isResetInNewCycle())
						reset();
				}
			}
		}
	}

	@Override
	public boolean canNewCycle(ChangeCycleVariant type, Object... params) {
		switch (type) {
			case VEHICLE_PROVIDER:
				if (params.length == 1 && checkVehicleProvider(params[0]) != null) {
					VehicleProvider vp = (VehicleProvider) params[0];
					if (vp.nextVehicle(false))
						return true;
				}
				return false;
		}
		return true;
	}

	@Override
	public void reset() {
		currentTime = 0l;
	}

	/**
	 * Checks if the object o is instance of Time Window of type Long. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Time Window of type Long if object o cast be casted, otherwise
	 *         null
	 */
	@SuppressWarnings("unchecked")
	private TimeWindow<Long> checkLongTimeWindow(Object o) {
		if (o instanceof HasTimeWindow<?> && ((HasTimeWindow<?>) o).getTimeWindow() != null) {
			TimeWindow<?> timeWindow = ((HasTimeWindow<?>) o).getTimeWindow();
			if (timeWindow.getStart() instanceof Long && timeWindow.getEnd() instanceof Long)
				return (TimeWindow<Long>) timeWindow;
		}
		return null;
	}
	
	/**
	 * Checks if the object o is instance of Time Delay of type Long. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Long if object o cast be casted, otherwise
	 *         null
	 */
	private Long checkLongTimeDelay(Object o) {
		if (o instanceof HasTimeDelay<?> && ((HasTimeDelay<?>) o).getTimeDelay() != null 
				&& ((HasTimeDelay<?>) o).getTimeDelay() instanceof Long)
			return (Long) ((HasTimeDelay<?>) o).getTimeDelay();
		return null;
	}
	
	/**
	 * Checks if the object o is instance of Destination. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Destination if object o cast be casted, otherwise
	 *         null
	 */
	private Destination checkDestination(Object o) {
		if (o instanceof Destination)
			return (Destination) o;
		return null;
	}
	
	/**
	 * Checks if the object o is instance of Vehicle Provider. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Vehicle Provider if object o cast be casted, otherwise
	 *         null
	 */
	private VehicleProvider checkVehicleProvider(Object o) {
		if (o instanceof VehicleProvider)
			return (VehicleProvider) o;
		return null;
	}
	
	/**
	 * Checks if the object o is instance of Route. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Route if object o cast be casted, otherwise
	 *         null
	 */
	private Route checkRoute(Object o) {
		if (o instanceof Route)
			return (Route) o;
		return null;
	}
	
	/**
	 * Checks if the object o is instance of Cycle. Returns
	 * null if not.
	 * 
	 * @param o
	 *            Object for check
	 * @return Cycle if object o cast be casted, otherwise
	 *         null
	 */
	private Cycle checkCycle(Object o) {
		if (o instanceof Cycle)
			return (Cycle) o;
		return null;
	}
}